Bitmap的加载和Cache(二)— 缓存策略

前言

ImageLoader系列第二篇,也是实现一个ImageLoader的核心。总体思想如下:当程序第一次从网上加载图片后,就将其缓存到设备上,这样下次使用这张图片的时候就不用从网络上重新下载,这样就为用户节省了流量。很多时候为了提高应用的用户体验,往往还会在内存中缓存一份,这样当应用打算显示一张图片,首先会从内存中获取,如果内存中没有就从存储设备获取,存储设备没有,就从网上获取。这是因为从内存中加载图片比从存储设备上加载图片要快,这样不仅提高了程序的效率又为用户节省了不必要的流量开支。

缓存策略包含缓存的添加、获取和删除。添加和获取很好理解,删除是因为缓存的大小是有限制的,当缓存容量满时,程序又要添加缓存,这个时候就要删除已有的缓存。如何定义缓存的新旧就是一种策略,不同的策略对应不同的缓存算法。目前比较常用的缓存算法是LRU(Least Recently Used),即近期最少使用算法。采用LRU算法的缓存有两种:LruCache和DiskLruCache。LruCache用于实现内存缓存,而DiskLruCache用于实现磁盘缓存。

下面就开始慢慢享受精神食粮。

LruCache

LruCache是一个泛型类,它内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加。当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象。关于强弱引用之前也说过,这里再简单说下:

  • 强引用:直接的对象引用
  • 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被GC回收
  • 弱引用:当一个对象只有弱引用存在时,此对象会随时被GC回收
1
2
3
4
5
6
7
int maxSize = (int) (Runtime.getRuntime().maxMemory() / 8);
mLruCache = new LruCache<String, Bitmap>(maxSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};

以当前应用程序最大内存的1/8作为内存缓存空间。

1
2
3
4
5
6
7
8
9
10
//添加缓存
private void putBitmapToCache(Bitmap bitmap, String url) {
if (bitmap != null) {
mLruCache.put(url, bitmap);
}
}
//获取缓存
private Bitmap getBitmapFromCache(String url) {
return mLruCache.get(url);
}

DiskLruCache

  1. DiskLruCache的创建

    DiskLruCache并不能通过构造方法来创建,它提供了open方法用于创建自身,接口如下:

    1
    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

    四个参数依次表示:缓存目录、版本号、同一个key对应多少个缓存文件和缓存总大小。

    缓存目录通常是 /sdcard/Android/data/< application package >/cache,但是我们又要考虑如果这个手机没有SD卡,或者SD卡被移除的情况,因此我们需要专门写一个方法来获取缓存地址。如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50; //50M
    public static final String url = "https://i.loli.net/2017/12/02/5a21fdf88aeef.jpg";
    /**
    * 返回一个DiskLruCache对象
    */
    public DiskLruCache open(Context mContext) {
    DiskLruCache diskLruCache = null;
    File cacheDir = getDiskCacheDir(mContext, "bitmap");
    if (!cacheDir.exists()) {
    cacheDir.mkdirs();
    }
    try {
    diskLruCache = DiskLruCache.open(cacheDir, 1, 1, DISK_CACHE_SIZE);
    } catch (IOException e) {
    e.printStackTrace();
    }
    return diskLruCache;
    }
    /**
    * 获取缓存目录
    */
    public File getDiskCacheDir(Context context, String uniqueName) {
    String cachePath;
    // SD卡存在或者SD卡不可被移除
    if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) {
    // 目录:/sdcard/Android/data/<application package>/cache
    cachePath = context.getExternalCacheDir().getPath();
    } else {
    // 目录:/data/data/<application package>/cache
    cachePath = context.getCacheDir().getPath();
    }
    return new File(cachePath + File.separator + uniqueName);
    }
  2. DiskLruCache的添加

    写入操作是借助DiskLruCache.Editor这个类完成的。类似的,这个类也是不能通过new来实例化的,需要调用DiskLruCache的edit()方法来获取实例,接口如下:

    1
    public Editor edit(String key) throws IOException

    这个方法接受一个参数key,这个key将会作为缓存文件的文件名,并且必须是要和图片的URL一一对应的。因为URL中可能有一些特殊符号,所以不适合用URL来作为key,因此最简单的办法就是用URL的MD5编码作为key。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    /**
    * 将字符串进行MD5编码
    */
    public String hashKeyForDisk(String key) {
    String cacheKey;
    try {
    final MessageDigest mDigest = MessageDigest.getInstance("MD5");
    mDigest.update(key.getBytes());
    cacheKey = bytesToHexString(mDigest.digest());
    } catch (NoSuchAlgorithmException e) {
    cacheKey = String.valueOf(key.hashCode());
    }
    return cacheKey;
    }
    public String bytesToHexString(byte[] bytes) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < bytes.length; i++) {
    String hex = Integer.toHexString(0xFF & bytes[i]);
    if (hex.length() == 1) {
    sb.append(0);
    }
    sb.append(hex);
    }
    return sb.toString();
    }

    因此,现在我们可以得到一个DiskLruCache.Editor的实例:

    1
    2
    3
    String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
    String key = hashKeyForDisk(imageUrl);
    DiskLruCache.Editor editor = mDiskLruCache.edit(key);

    有了DiskLruCache.Editor的实例之后,我们可以调用它的newOutputStream()方法来创建一个输出流,然后把它传入到downloadUrlToStream()中就能实现下载并写入缓存的功能。newOutputStream()方法接受一个index的参数,由于前面在设置valueCount的时候指定的是1,所以这里index传入0就可以了。在写入操作执行完之后,我们还需要调用commit()方法进行提交才能使写入生效,调用about()方法则表示放弃此次写入。不过下载时候需要在非UI线程中执行,所以这里用到了AsyncTask。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    @Override
    protected Boolean doInBackground(Object... objects) {
    DiskLruCacheUtil util = new DiskLruCacheUtil();
    String key = util.hashKeyForDisk(util.url);
    DiskLruCache diskLruCache = (DiskLruCache) objects[0];
    try {
    DiskLruCache.Editor editor = diskLruCache.edit(key);
    if (editor != null) {
    OutputStream outputStream = editor.newOutputStream(0);
    if (downloadUrlToStream(util.url, outputStream)) {
    publishProgress("");
    editor.commit();
    } else {
    editor.abort();
    }
    }
    } catch (IOException e) {
    e.printStackTrace();
    }
    return null;
    }
    /**
    * 下载
    */
    private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
    HttpURLConnection urlConnection = null;
    BufferedOutputStream out = null;
    BufferedInputStream in = null;
    try {
    final URL url = new URL(urlString);
    urlConnection = (HttpURLConnection) url.openConnection();
    in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
    out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
    int b;
    while ((b = in.read()) != -1) {
    out.write(b);
    }
    return true;
    } catch (Exception e) {
    Log.e(TAG, "Error in downloadBitmap - " + e);
    } finally {
    if (urlConnection != null) {
    urlConnection.disconnect();
    }
    try {
    if (out != null) {
    out.close();
    }
    if (in != null) {
    in.close();
    }
    } catch (final IOException e) {
    }
    }
    return false;
    }

    执行的时候传入DiskLruCache实例就好了,如下:

    1
    2
    mDiskLruCache = new DiskLruCacheUtil().open(MainActivity.this);
    new LoadAsyncTask(this).execute(mDiskLruCache);
  3. DiskLruCache的读取

    读取相对于存储来说就很简单了,主要是借助DiskLruCache的get()方法来实现,接口如下:

    1
    public synchronized Snapshot get(String key) throws IOException

    传入的参数key就是将URL进行MD5编码后的值了,返回的是一个Snapshot快照对象,然后调用它的getInputStream就得到文件的输入流了,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private Bitmap getCacheFromDisk() {
    DiskLruCacheUtil util = new DiskLruCacheUtil();
    String key = util.hashKeyForDisk(util.url);
    try {
    DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
    if (snapshot != null) {
    InputStream in = snapshot.getInputStream(0);
    return BitmapFactory.decodeStream(in);
    }
    } catch (IOException e) {
    e.printStackTrace();
    }
    return null;
    }

至此,整个DiskLruCache就已经实现完了。

最后

感谢郭神的博客对于DiskLruCache的详细解析,原文:Android DiskLruCache完全解析,硬盘缓存的最佳方案

本篇文章全部代码:https://github.com/Omooo/CacheDemo

我们一直都向往,面朝大海,春暖花开。 但是几人能做到,心中有爱,四季不败?